Using Character Classes, Quantifiers, and Groups in Python Regular Expressions
Course: Python Training
Topic: Using Character Classes, Quantifiers, and Groups
Course URL: Python Training Course
Demo URL: Register for a Course Demo
Learning Platform: JustAcademy Official Website
1. Introduction to Regular Expressions
Regular expressions, commonly called regex or regexp, are patterns used to search, match, extract, split, and replace text. Python provides the built-in re module for working with regular expressions.
Character classes, quantifiers, and groups are three important regex features. Character classes define which characters are allowed, quantifiers specify how many times a pattern can occur, and groups organize parts of a pattern so they can be matched, extracted, or referenced.
These features are useful for validating input, extracting information from documents, processing logs, cleaning datasets, checking text formats, and searching large amounts of text.
Example
import re
text = "Order ID: AB1234"
pattern = r"[A-Z]{2}\d{4}"
match = re.search(pattern, text)
if match:
print(match.group())
# Output:
# AB1234
In this example, [A-Z] matches an uppercase English letter, {2} requires exactly two such letters, \d matches a digit, and {4} requires four digits.
2. Learning Objectives
- Understand regular expressions and Python's
re module.
- Use character classes to match specific sets of characters.
- Apply shorthand character classes such as
\d, \w, and \s.
- Use quantifiers to control the number of repetitions.
- Understand greedy and non-greedy matching.
- Use capturing and non-capturing groups.
- Apply alternation to match alternative patterns.
- Extract specific data with groups and named groups.
- Combine regex features to validate and extract structured text.
- Identify common regex errors and write maintainable patterns.
3. Getting Started with Python's re Module
Import the re module before using its regular expression functions. In most cases, write regex patterns as raw strings by adding the prefix r. Raw strings make backslashes easier to read because Python does not process most escape sequences inside them in the usual way.
import re
pattern = r"\d+"
text = "I have 25 books."
result = re.search(pattern, text)
if result:
print(result.group())
# Output:
# 25
Common Regex Functions
| Function |
Purpose |
Example |
re.search() |
Finds the first match anywhere in a string. |
re.search(r"\d+", text) |
re.match() |
Checks for a match at the beginning of a string. |
re.match(r"Hello", text) |
re.fullmatch() |
Checks whether the entire string matches the pattern. |
re.fullmatch(r"\d+", text) |
re.findall() |
Returns all non-overlapping matches. |
re.findall(r"\d+", text) |
re.finditer() |
Returns an iterator of match objects. |
re.finditer(r"\d+", text) |
re.sub() |
Replaces matching text. |
re.sub(r"\d+", "#", text) |
re.split() |
Splits text using a matching pattern. |
re.split(r"[,;]", text) |
4. Understanding Character Classes
A character class specifies a set of characters that can match at a particular position in the text. In Python regex, a character class is usually written inside square brackets [].
4.1 Match a Single Character from a Set
import re
text = "cat bat rat mat"
matches = re.findall(r"[cb]at", text)
print(matches)
# Output:
# ['cat', 'bat']
The pattern [cb] matches either c or b. Therefore, [cb]at matches cat and bat, but not rat or mat.
4.2 Match a Range of Characters
A hyphen can specify a range inside a character class.
| Pattern |
Meaning |
Example Matches |
[a-z] |
One lowercase English letter |
a, m, z |
[A-Z] |
One uppercase English letter |
A, M, Z |
[0-9] |
One ASCII digit |
0, 5, 9 |
[a-zA-Z] |
One English letter, either case |
A, b |
[a-zA-Z0-9] |
One English letter or ASCII digit |
R, 7 |
import re
text = "Python 3 is Great!"
letters = re.findall(r"[A-Z]", text)
digits = re.findall(r"[0-9]", text)
lowercase = re.findall(r"[a-z]", text)
print(letters) # ['P', 'G']
print(digits) # ['3']
print(lowercase) # ['y', 't', 'h', 'o', 'n', 'i', 's', 'r', 'e', 'a', 't']
4.3 Character Classes with Multiple Allowed Characters
Include each allowed character inside the brackets.
import re
text = "color colour"
matches = re.findall(r"colou?r", text)
print(matches)
# Output:
# ['color', 'colour']
The example above demonstrates an optional character with ?. A character class can also be used to match any one of several letters:
import re
text = "color colour"
matches = re.findall(r"colo[u]?r", text)
print(matches)
# Output:
# ['color', 'colour']
4.4 Negated Character Classes
When a character class begins with ^, it matches any character except those listed inside the brackets.
import re
text = "abc123!@"
matches = re.findall(r"[^0-9]", text)
print(matches)
# Output:
# ['a', 'b', 'c', '!', '@']
Here, [^0-9] matches each character that is not an ASCII digit. It does not mean “match the beginning of the string”; that meaning belongs to ^ outside a character class.
4.5 Special Characters Inside Character Classes
Some characters behave differently inside square brackets than they do outside them. For example, the dot . is a literal dot inside a character class.
import re
text = "a.b a-b a+b"
matches = re.findall(r"[.+-]", text)
print(matches)
# Output:
# ['.', '-', '+']
Place a hyphen at the beginning or end of a character class, or escape it, when you intend to match a literal hyphen. A caret is literal when it is not in the first position of the class.
5. Shorthand Character Classes
Python regex provides shorthand classes for frequently used character categories. Their meaning can depend on whether the pattern is Unicode-aware or uses ASCII mode.
| Pattern |
Default Python Meaning |
Example |
\d |
Any Unicode decimal digit |
7, ٣ |
\D |
Any character that is not a Unicode decimal digit |
A, ! |
\w |
Unicode alphanumeric characters plus underscore |
A, é, 5, _ |
\W |
Any character not matched by \w |
!, - |
\s |
Whitespace characters |
Space, tab, newline |
\S |
Any non-whitespace character |
A, 8, ! |
5.1 Matching Digits
import re
text = "Order 123, quantity 45"
print(re.findall(r"\d+", text))
# Output:
# ['123', '45']
The + quantifier matches one or more consecutive digits. In Python's default Unicode mode, \d can match digits beyond ASCII 0 through 9. Use [0-9] or an ASCII-mode pattern when only ASCII digits are permitted.
5.2 Matching Word Characters
import re
text = "user_name-25!"
print(re.findall(r"\w+", text))
# Output:
# ['user_name', '25']
Word characters include letters, digits, and underscores, with Unicode support by default. Hyphens and exclamation marks are not word characters.
5.3 Matching Whitespace
import re
text = "Python\\tRegex\\nGuide"
parts = re.split(r"\s+", text)
print(parts)
# Output:
# ['Python', 'Regex', 'Guide']
Whitespace matching is useful for splitting text that may contain spaces, tabs, or newline characters.
6. Understanding Quantifiers
Quantifiers specify how many times the preceding character, character class, or group can occur. They are essential for matching text of variable lengths.
| Quantifier |
Meaning |
Example |
* |
Zero or more occurrences |
a* |
+ |
One or more occurrences |
a+ |
? |
Zero or one occurrence |
a? |
{n} |
Exactly n occurrences |
a{3} |
{n,} |
At least n occurrences |
a{2,} |
{n,m} |
Between n and m occurrences, inclusive |
a{2,5} |
6.1 The Asterisk *
The asterisk matches zero or more occurrences of the preceding pattern.
import re
text = "color colour colouur"
matches = re.findall(r"colou*r", text)
print(matches)
# Output:
# ['color', 'colour', 'colouur']
In colou*r, the letter u may occur zero or more times.
6.2 The Plus +
The plus quantifier matches one or more occurrences.
import re
text = "a aa aaa b"
matches = re.findall(r"a+", text)
print(matches)
# Output:
# ['a', 'aa', 'aaa']
Unlike *, the plus quantifier requires at least one occurrence of the preceding pattern.
6.3 The Question Mark ?
The question mark makes the preceding pattern optional, allowing zero or one occurrence.
import re
text = "organize organise"
matches = re.findall(r"organi[sz]e", text)
print(matches)
# Output:
# ['organize', 'organise']
This example uses a character class to permit either s or z. An optional character can instead be expressed with ? when one particular character may or may not appear.
import re
text = "color colour"
matches = re.findall(r"colou?r", text)
print(matches)
# Output:
# ['color', 'colour']
6.4 Exact Repetition with {n}
import re
text = "A12 AB123 ABC1234"
matches = re.findall(r"[A-Z]{2}", text)
print(matches)
# Output:
# ['AB', 'BC']
The pattern [A-Z]{2} matches exactly two consecutive uppercase letters. findall() searches for non-overlapping matches, so the letters in ABC can produce AB and then BC depending on the remaining text and matching positions.
6.5 Minimum Repetition with {n,}
import re
text = "a aa aaa aaaa"
matches = re.findall(r"a{2,}", text)
print(matches)
# Output:
# ['aa', 'aaa', 'aaaa']
The pattern matches groups of at least two consecutive lowercase a characters.
6.6 Bounded Repetition with {n,m}
import re
text = "7 42 123 12345"
matches = re.findall(r"\b\d{1,3}\b", text)
print(matches)
# Output:
# ['7', '42', '123']
The expression \d{1,3} matches one to three digits, and \b requires word boundaries around the matched number. The four-digit number is excluded because it cannot be matched as a complete one-to-three-digit number with boundaries at both ends.
7. Greedy and Non-Greedy Quantifiers
By default, quantifiers such as *, +, and {n,m} are greedy: they try to match as much text as possible while allowing the overall pattern to succeed. Adding ? after a quantifier makes it non-greedy, so it tries to match as little text as possible.
7.1 Greedy Matching
import re
text = "<b>First</b> and <b>Second</b>"
matches = re.findall(r"<b>.*</b>", text)
print(matches)
# Output:
# ['<b>First</b> and <b>Second</b>']
The .* expression can consume the text between the first opening tag and the last closing tag, producing one long match.
7.2 Non-Greedy Matching
import re
text = "<b>First</b> and <b>Second</b>"
matches = re.findall(r"<b>.*?</b>", text)
print(matches)
# Output:
# ['<b>First</b>', '<b>Second</b>']
The .*? expression matches as little text as possible before the closing pattern is found. For complex HTML or XML, use a dedicated parser instead of relying on regex alone.
8. Understanding Groups
Parentheses () create groups in a regular expression. Groups let you treat multiple characters or expressions as one unit, capture a substring, apply a quantifier to a sequence, and refer to matched portions.
8.1 Capturing Groups
import re
text = "Date: 2026-10-22"
pattern = r"(\d{4})-(\d{2})-(\d{2})"
match = re.search(pattern, text)
if match:
print(match.group(0))
print(match.group(1))
print(match.group(2))
print(match.group(3))
# Output:
# 2026-10-22
# 2026
# 10
# 22
group(0) returns the entire match. group(1), group(2), and group(3) return the captured year, month, and day.
8.2 Groups for Repeating a Sequence
Quantifiers can be applied to a complete group rather than to a single character.
import re
text = "ha hahaha"
matches = re.findall(r"(ha)+", text)
print(matches)
# Output:
# ['ha', 'ha']
The group (ha) represents the sequence of two characters. The plus quantifier allows the sequence to repeat one or more times. When a capturing group is repeated, the captured value returned by findall() is the final captured occurrence within each match, not necessarily the entire matched text.
8.3 Non-Capturing Groups
Use (?:...) when you need to group part of a pattern but do not need to capture its value.
import re
text = "color colour"
matches = re.findall(r"col(?:o)?ur|colou?r", text)
print(matches)
A simpler pattern for this example is colou?r. Non-capturing groups are most useful when grouping is necessary for alternation or quantification, but capturing the group's contents is not.
8.4 Named Capturing Groups
Named groups give captured values descriptive names, making complex patterns easier to understand and maintain.
import re
text = "Order ID: AB1234"
pattern = r"(?P<prefix>[A-Z]{2})(?P<number>\d{4})"
match = re.search(pattern, text)
if match:
print(match.group("prefix"))
print(match.group("number"))
print(match.groupdict())
# Output:
# AB
# 1234
# {'prefix': 'AB', 'number': '1234'}
Named groups are particularly useful when extracting structured data such as dates, transaction IDs, log fields, and product codes.
9. Alternation with the Pipe Symbol
The pipe symbol | means “or.” It allows a pattern to match one alternative or another. Parentheses can make the alternatives operate as a single unit.
import re
text = "Python Java JavaScript C++"
matches = re.findall(r"Python|JavaScript|Java|C\+\+", text)
print(matches)
# Output:
# ['Python', 'Java', 'JavaScript', 'C++']
Order can matter when alternatives overlap because the regex engine tries alternatives from left to right. In this example, JavaScript is placed before Java so that the longer alternative is considered first.
Grouped Alternatives
import re
text = "colour color"
matches = re.findall(r"colou?r", text)
print(matches)
# Output:
# ['colour', 'color']
Use parentheses to group alternatives when they are part of a larger pattern, for example (cat|dog)s? to match cat, cats, dog, or dogs.
10. Combining Character Classes, Quantifiers, and Groups
Real-world patterns usually combine several regex features. Character classes define allowed characters, quantifiers define repetition, and groups organize related components.
10.1 Product Code
import re
text = "Product: AB-12345"
pattern = r"([A-Z]{2})-(\d{5})"
match = re.search(pattern, text)
if match:
print("Full code:", match.group(0))
print("Category:", match.group(1))
print("Number:", match.group(2))
# Output:
# Full code: AB-12345
# Category: AB
# Number: 12345
10.2 Username Format Example
import re
def is_valid_username(username):
"""Check a simple username format."""
pattern = r"[A-Za-z][A-Za-z0-9_]{2,15}"
return re.fullmatch(pattern, username) is not None
print(is_valid_username("alex_01")) # True
print(is_valid_username("7alex")) # False
print(is_valid_username("ab")) # False
This example requires an English letter first, followed by two to fifteen English letters, digits, or underscores. Therefore, the total length is three to sixteen characters. Real application rules should be documented and aligned with the product requirements.
10.3 Extracting Dates
import re
text = "Created: 2026-10-22"
pattern = (
r"(?P<year>\d{4})-"
r"(?P<month>\d{2})-"
r"(?P<day>\d{2})"
)
match = re.search(pattern, text)
if match:
print(match.groupdict())
# Output:
# {'year': '2026', 'month': '10', 'day': '22'}
This pattern checks the structure of a date, not whether the date is valid on a calendar. For example, a structurally matching date can still have an invalid month or day. Use datetime for calendar validation after extracting the fields.
11. Practical Example: Extracting Information from Logs
Suppose an application log contains a severity level, a date, and a message. Named groups can extract each field into a structured result.
import re
log_line = (
"[ERROR] 2026-10-22 "
"Database connection failed"
)
pattern = (
r"\[(?P<level>[A-Z]+)\]\s+"
r"(?P<date>\d{4}-\d{2}-\d{2})\s+"
r"(?P<message>.+)"
)
match = re.fullmatch(pattern, log_line)
if match:
log_data = match.groupdict()
print(log_data)
else:
print("Log format is not recognized.")
# Output:
# {'level': 'ERROR',
# 'date': '2026-10-22',
# 'message': 'Database connection failed'}
The expression uses a character class for the uppercase severity level, quantifiers to define field lengths, whitespace matching to separate fields, and named groups to capture the values.
12. Common Mistakes and Troubleshooting
| Mistake |
Problem |
Better Approach |
Using * when at least one character is required |
The pattern can match an empty string. |
Use + when one or more occurrences are required. |
| Forgetting raw strings |
Backslashes may be interpreted by Python string syntax. |
Prefer patterns such as r"\d+". |
| Using a broad character class |
Unexpected characters may be accepted. |
Define the allowed character set explicitly. |
| Using greedy matching unintentionally |
The match may include more text than expected. |
Consider a non-greedy quantifier or a more precise pattern. |
| Capturing every group unnecessarily |
Results can become harder to manage. |
Use non-capturing groups where appropriate. |
Assuming \d means only ASCII digits |
Unicode decimal digits can also match. |
Use [0-9] or ASCII mode if required. |
Using search() for full validation |
A valid substring may match inside invalid input. |
Use fullmatch() for whole-string validation. |
| Assuming a regex guarantees semantic validity |
Structurally valid text may still be invalid data. |
Apply domain-specific validation after matching. |
| Writing an extremely complicated pattern |
The expression becomes difficult to review and maintain. |
Break the pattern into named components and document its rules. |
13. Best Practices for Writing Maintainable Regex
- Use raw strings such as
r"\d{4}" for regex patterns.
- Choose character classes that match the actual input requirements.
- Use quantifiers that express minimum and maximum lengths clearly.
- Use named groups for structured data extraction.
- Use non-capturing groups when capture results are unnecessary.
- Prefer
re.fullmatch() when validating an entire input string.
- Test valid inputs, invalid inputs, boundary lengths, and Unicode cases where relevant.
- Use
re.VERBOSE for long patterns that benefit from whitespace and comments.
- Do not assume that matching a pattern is equivalent to validating all business rules.
- Avoid unnecessarily complex patterns and patterns that can cause excessive backtracking on untrusted input.
Using re.VERBOSE
import re
pattern = re.compile(
r"""
(?P<prefix>[A-Z]{2}) # Two uppercase letters
- # A literal hyphen
(?P<number>\d{4}) # Four digits
""",
re.VERBOSE
)
match = pattern.fullmatch("AB-1234")
if match:
print(match.groupdict())
# Output:
# {'prefix': 'AB', 'number': '1234'}
In verbose mode, whitespace outside character classes is ignored and comments can explain the pattern. If a literal space is required, represent it explicitly, for example with \s or an escaped space where appropriate.
14. Hands-On Exercises
- Character classes: Extract all uppercase English letters from a sentence.
- Digit matching: Find all integer sequences in a paragraph.
- Negated classes: Extract all characters except digits from an input string.
- Quantifiers: Match words containing between four and eight English letters.
- Optional characters: Match both
color and colour.
- Capturing groups: Extract the year, month, and day from a date string.
- Named groups: Extract a product prefix and numeric identifier from
XY-54321.
- Alternation: Match the words
Python, Java, or Ruby.
- Greedy matching: Compare
.* with .*? on text containing multiple bracketed sections.
- Validation: Create a regex for a username with a letter at the beginning and a defined length.
- Log parsing: Extract severity, date, and message fields from a sample log line.
- Testing: Test your patterns against empty strings, incorrect lengths, punctuation, and Unicode characters.
15. Interview Questions and Answers
Q1. What is a regular expression?
A regular expression is a pattern used to match, search, extract, split, or replace text.
Q2. What is a character class?
A character class, usually written inside square brackets, defines the set of characters allowed at one position in a match. For example, [A-Z] matches one uppercase English letter.
Q3. What is the difference between \d and [0-9]?
In Python's default Unicode mode, \d matches Unicode decimal digits, while [0-9] matches ASCII digits from zero through nine.
Q4. What does \w match?
By default, \w matches Unicode alphanumeric characters and the underscore character.
Q5. What is the difference between * and +?
* matches zero or more occurrences, while + matches one or more occurrences of the preceding pattern.
Q6. What does ? mean in a regex?
As a quantifier, it matches zero or one occurrence of the preceding pattern. When placed after a quantifier such as * or +, it makes that quantifier non-greedy.
Q7. What is the purpose of {n,m}?
It specifies a minimum of n and a maximum of m repetitions, inclusive.
Q8. What is a capturing group?
A capturing group, written using parentheses, stores the portion of text matched by that group so it can be retrieved from the match object.
Q9. What is a non-capturing group?
A non-capturing group uses (?:...). It groups a pattern without creating a numbered capture.
Q10. What is a named group?
A named group uses syntax such as (?P<year>\d{4}), allowing the captured value to be accessed by its descriptive name.
Q11. What is greedy matching?
Greedy quantifiers attempt to match as much text as possible while still allowing the overall pattern to match.
Q12. What is non-greedy matching?
Non-greedy quantifiers, such as *? and +?, attempt to match as little text as possible while allowing the overall pattern to succeed.
Q13. What is the difference between re.search() and re.fullmatch()?
re.search() finds a matching substring anywhere in the input. re.fullmatch() succeeds only when the entire input string matches the pattern.
Q14. Why are raw strings useful for regex patterns?
Raw strings reduce confusion between Python string escaping and regex escaping, especially for patterns containing backslashes.
Q15. Can regular expressions validate every type of input?
No. Regex is useful for checking text structure, but some rules require additional logic. For example, matching a date's digit format does not prove that the date exists on a calendar.
16. Quick Revision Checklist
[] defines a character class.
[^...] matches characters outside the specified class.
\d, \w, and \s are useful shorthand classes.
* means zero or more repetitions.
+ means one or more repetitions.
? means zero or one repetition.
{n}, {n,}, and {n,m} specify repetition counts.
- Parentheses create capturing groups.
(?:...) creates a non-capturing group.
(?P<name>...) creates a named group.
| represents alternatives.
- A greedy quantifier tries to match as much as possible.
- A non-greedy quantifier tries to match as little as possible.
- Use
re.fullmatch() for whole-string format validation.
- Test regex patterns against both valid and invalid inputs.
17. Conclusion
Character classes, quantifiers, and groups form the foundation of practical regular expression work in Python. Character classes control which characters are allowed, quantifiers specify repetition, and groups organize or capture meaningful parts of a match.
By combining these features with functions from Python's re module, developers can build patterns for text extraction, input validation, log processing, and data cleaning. The best regex patterns are not merely capable of matching text; they are also clear, tested, and aligned with the application's actual requirements.
18. Additional Learning Resources